/*
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.dspl.validate;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.dspl.model.Concept.ConceptProperty;
import com.google.dspl.model.DataType;
import com.google.dspl.model.Identifier;

import java.util.List;
import java.util.Set;

/**
 * Performs various operations on a concept by traversing up the inheritance tree if needed.
 *
 * @author Shardul Deo
 */
public class ConceptInheritenceTraverser {

  private ConceptInheritenceTraverser() {
  }

  /**
   * Represents a concept over which operations are performed.
   */
  public interface ConceptNode {

    /**
     * @return Identifier of the concept.
     */
    Identifier getId();

    /**
     * @return Parent concept if present, null otherwise
     */
    ConceptNode getParent();

    /**
     * @return If the give parentId is the parent of this concept
     */
    boolean isParent(Identifier parentId);

    /**
     * @return Data type of the concept if present, null otherwise
     */
    DataType getType();

    /**
     * @return Data type of the attribute with the given id if present, null otherwise
     */
    DataType getAttributeType(String attributeId);

    /**
     * @return Referenced concept by the attribute with the given id if present, null otherwise
     */
    Identifier getConceptReferencedByAttribute(String attributeId);

    /**
     * @return Data type of the property with the given id if present, null otherwise
     */
    DataType getPropertyType(String propertyId);

    /**
     * @return List of properties
     */
    List<ConceptProperty> getProperties();

    /**
     * @return Name of the data table of the concept
     */
    String getTable();
  }

  @VisibleForTesting
  interface ConceptOperation<T> {
    T operate(ConceptNode concept);
  }

  @VisibleForTesting
  static <T> T resolveInheritance(ConceptNode conceptNode, ConceptOperation<T> operation) {
    Set<Identifier> visitedConcepts = Sets.newHashSet();

    while (conceptNode != null) {
      if (visitedConcepts.contains(conceptNode.getId())) {
        // Cycle detected, we stop here.
        return null;
      }

      visitedConcepts.add(conceptNode.getId());

      T result = operation.operate(conceptNode);
      if (result != null) {
        return result;
      }

      conceptNode = conceptNode.getParent();
    }
    return null;
  }

  /**
   * @return If the give parentId is in the ancestor chain of the concept.
   */
  public static boolean isParent(ConceptNode conceptNode, final Identifier parentId) {
    Boolean result = resolveInheritance(conceptNode, new ConceptOperation<Boolean>() {
      @Override
      public Boolean operate(ConceptNode concept) {
        if (concept.isParent(parentId)) {
          return true;
        }
        // We want to continue up the chain, so return null
        return null;
      }
    });
    return result == null ? false : result;
  }

  /**
   * @return Data type of the concept, either explicitly specified or inferred through its
   *         inheritance chain, or null if the type cannot be inferred.
   */
  public static DataType resolveConceptType(ConceptNode conceptNode) {
    return resolveInheritance(conceptNode, new ConceptOperation<DataType>() {
      @Override
      public DataType operate(ConceptNode concept) {
        return concept.getType();
      }
    });
  }

  /**
   * @return Table of the concept, either explicitly specified or inferred through its
   *         inheritance chain, or null a table cannot be inferred.
   */
  public static Identifier resolveConceptTable(ConceptNode conceptNode) {
    return resolveInheritance(conceptNode, new ConceptOperation<Identifier>() {
      @Override
      public Identifier operate(ConceptNode concept) {
        return (concept.getTable() != null)
            ? new Identifier(concept.getId().getDatasetId(), concept.getTable())
            : null;
      }
    });
  }

  /**
   * @return Data type of an attribute in the given concept, either explicitly specified or
   *         inferred through its inheritance chain, or null if the type cannot be inferred.
   */
  public static DataType resolveConceptAttributeType(
      final String attributeId, ConceptNode conceptNode) {
    return resolveInheritance(conceptNode, new ConceptOperation<DataType>() {
      @Override
      public DataType operate(ConceptNode concept) {
        return concept.getAttributeType(attributeId);
      }
    });
  }

  /**
   * @return Concept referenced by an attribute in the given concept, either explicitly specified or
   *         inferred through its inheritance chain, or null if it cannot be inferred.
   */
  public static Identifier resolveConceptAttributeReferencedConcept(
      final String attributeId, ConceptNode conceptNode) {
    return resolveInheritance(conceptNode, new ConceptOperation<Identifier>() {
      @Override
      public Identifier operate(ConceptNode concept) {
        return concept.getConceptReferencedByAttribute(attributeId);
      }
    });
  }

  /**
   * @return Data type of a property in the given concept, either explicitly specified or inferred
   *         through its inheritance chain, or null if the type cannot be inferred.
   */
  public static DataType resolveConceptPropertyType(
      final String propertyId, ConceptNode conceptNode) {
    return resolveInheritance(conceptNode, new ConceptOperation<DataType>() {
      @Override
      public DataType operate(ConceptNode concept) {
        return concept.getPropertyType(propertyId);
      }
    });
  }

  /**
   * @return the property information defined in the youngest concept that defines the property.
   *     It does not include any property information from any parent concept.
   */
  public static ConceptProperty getConceptProperty(ConceptNode conceptNode, final String propertyId) {
    return resolveInheritance(conceptNode, new ConceptOperation<ConceptProperty>() {
      @Override
      public ConceptProperty operate(ConceptNode concept) {
        for (ConceptProperty property : concept.getProperties()) {
          if (propertyId.equals(property.getId())) {
            return property;
          }
        }

        return null;
      }
    });
  }

  /**
   * Fetch all of the properties of the given concept, including the properties of its parent
   * concepts.  These properties are returned in a multimap keyed on the id of the concept.
   *
   * @param conceptNode Concept whose properties are desired.
   * @return Multimap of concept properties, keyed by the concept that defines that property.
   *
   * TODO: Determine a better API for this notion, especially around the inheritance of properties.
   */
  public static Multimap<Identifier, ConceptProperty> getPropertiesIncludingInherited(
      ConceptNode conceptNode) {
    ConceptOperationImplementation operation = new ConceptOperationImplementation();
    resolveInheritance(conceptNode, operation);
    return Multimaps.unmodifiableMultimap(operation.properties);
  }

  private static final class ConceptOperationImplementation implements ConceptOperation<Void> {
    final Multimap<Identifier, ConceptProperty> properties = HashMultimap.create();

    @Override
    public Void operate(ConceptNode concept) {
      properties.putAll(concept.getId(), concept.getProperties());

      // Always returns null, since we want to traverse the whole chain.
      return null;
    }
  }
}
